最近在做一个基于Android Lint的自定义静态代码检查功能库,这里做一个简单的总结。前半部分介绍SDK自带Android Lint的功能与配置使用方法,后半部分介绍扩展自定义Lint规则库的开发流程。
关于Lint的一些基本知识,以及自定义Lint如何实现,可以参考我的系列文章:
Android Lint工作原理剖析
浅谈Android自定义Lint规则的实现 (一)
浅谈Android自定义Lint规则的实现 (二)
相关Demo代码可以参见我的github代码库:
CustomLintDemo
什么是Android Lint
Android Lint是一个静态代码分析工具,它能够对你的Android项目中潜在的bug、可优化的代码、安全性、性能、可用性、可访问性、国际化等进行检查。
在Android SDK Tools 16及更高的版本中,Lint工具会自动安装。通过它对Android工程源代码进行扫描和检查,可发现潜在的问题,以便程序员及早修正这个问题。Android Lint提供了命令行方式执行,还与IDE(如Android Studio)进行了集成,并提供了xml和html形式的输出报告。
看了上面的介绍可能大家依然很迷惑“这货到底有啥用”,其实我们平时在Android开发过程中一直在享受Lint带来的便利。比如,下面图中的警告和错误提示,相信大家应该很熟悉吧:
上面的例子分别是java文件与Manifest文件在接受Lint检查后给出的简要lint报告,是Lint与IDE集成后的一种表现形式。事实上,Android Lint目前能检查的项目已经多达220项,检查的范围涵盖了二进制资源文件、java源代码、class文件、gradle配置文件、xml文件、resource文件夹、其他文件等。除了图中这种与IDE结合的简洁报告形式以外,也提供更详细的html和xml形式的报告,让你对自己代码质量的提升空间有更全面的认识。
在Android Studio中,每一次编译程序时都会自动运行lint分析工具,也可以在需要lint分析的文件夹、包或文件上点击右键选择【Analyze】->【Inspect Code】。 生成的报告包含了检查过程中发现的问题,并把这些内容按照类别、优先级、严重程度进行了区分。
Lint工具的处理流程如下图所示:
图中各部分含义如下:
- Application source files: 构成你Android project的源文件,包含Java和XML文件,图标,以及ProGuard配置文件。
- lint.xml: 配置文件,用来指定你想禁用哪些lint检查功能,以及自定义问题严重度(problem severity levels)。
- lint Tool: 一个可以从命令行或Android Studio中运行的静态打码扫描工具。
- lint Output: lint检查的结果,可以在命令行中通过lint查看,也可以在Android Studio的Event Log中查看。
Android Lint检查哪些内容
Android Lint内置了很多lint规则,到现在为止是220项检查,总共可以分为以下几类:
- Correctness 正确性
- Security 安全性
- Performance 性能
- Usability 可用性
- Accessibility 可访问性
- Internationalization 国际化
下面列举一些常见的lint会检测的代码问题:
- 缺少翻译(和未使用的翻译)
- 布局性能问题(老的layoutopt工具会用于查找所有这样的问题,和除此之外更多的问题)
- 未使用的资源
- 不一致的数组大小(当在多个配置中定义数组)
- 可访问性和国际化问题(硬编码字符串,缺少contentDescription等)
- 图标问题 (如丢失密度、 重复图标、 错误尺寸等)
- 可用性问题 (如不在文本字段上指定输入的类型)
- 清单错误
如果要查看lint工具支持的issue的完整列表和它们所对应的issue ID,可以使用
lint --list
命令。
配置Android Lint
默认情况下,当你运行Lint扫描时,它会对Lint支持的所有issue进行检查。你也可以限制只让lint检查特定的issue,并为某些issue分配严重度(severity level)。
比如,你可以禁止lint检查那些与你的项目无关的issue,并为lint配置一个更低的severity level来让它报告那些不是非常严重的issue。
你可以为lint检查配置不同的level:
- 全局(对整个project)
- 每个project module
- 每个production module
- 每个test module
- 每个open files
- 每个class hierarchy
- 每个Version Control System (VCS) scopes
在Android Studio中配置Lint
Android Studio允许你对lint每项检查单独启用或禁用,还可以对项目全局、特定文件夹、特定文件进行专门的lint配置。方法是在Android Studio中点击File > Settings > Project Settings
菜单打开Editor->Inspections
页面,里面有它支持的Profiles和Inspections列表,如图:
配置Lint文件
你可以在lint.xml文件中指定你对lint检查的偏好设置。如果你要手动创建这个文件,就把它放在你的Android工程的根目录中。如果你是在Android Studio中配置lint偏好,那么lint.xml文件会自动创建并添加到你的Android工程中。
lint.xml文件的组成结构是,最外面是一对闭合的
1 | <?xml version="1.0" encoding="UTF-8"?> |
通过设置
一个实例lint.xml文件如下所示:
1 | <?xml version="1.0" encoding="UTF-8"?> |
在Java源文件或XML源文件中配置lint检查
在Java中配置lint检查
要对Android项目中某个Java类或方法禁用lint检查,只需要对那段代码添加@SuppressLint
注解即可。
下面的例子显示了如何对onCreate方法关闭NewApi这个issue的lint检查。lint工具仍然会对这个类的其他方法进行NewApi issue的检查。例子如下:
1 | "NewApi") ( |
下面的例子显示如何对FeedProvider类关闭ParserError issue的lint检查:
1 | "ParserError") ( |
如果要在Java文件中禁用所有issue的lint检查,使用all
关键字,比如:
1 | "all") ( |
在XML中配置lint检查
如果要对XML文件中某一部分禁用lint检查,可以使用tools:ignore
属性来标识。为了让这个属性能够被lint工具识别,必须把下面的命名空间加入你的XML中:
1 | namespace xmlns:tools="http://schemas.android.com/tools" |
下面的例子显示了如何对XML布局文件中的UnusedResources
issue的lint检查。ignore
属性会被该元素下的子元素继承,在这个例子中,子元素
1 | <LinearLayout |
要禁用多个issue时,用逗号把它们分隔开,比如:
1 | tools:ignore="NewApi,StringFormatInvalid" |
要在某个XML元素中对所有issue都禁用lint检查,可以使用all
关键字,比如:
1 | tools:ignore="all" |
自定义lint
为什么需要自定义lint
由于每个项目自身的需求,Android Lint默认的检查项目可能不能满足我们的需求。
比如我们自己写了一个下拉刷新的库项目,可以让用户直接在xml布局文件中去使用它,但是我们希望用户必须在这个xml元素中定义一个pullmode
属性,否则组件无法正常运行,我们希望lint能够对此进行检查,并在用户忘记添加此属性时给出明确的错误提示。再比如,我们的项目中使用了自己封装的日志库,能够方便的在release版本中关闭日志输出来防止app的效率下降,该日志库还能够把日志输出到指定的文件中方便事后分析,这时有一位新成员加入了我们的开发,他可能还是习惯性的用android.util.Log来打印日志,我们希望能够检测到本项目中所有使用了android.util.Log的代码,并发出警告。
要满足这些自定义需求,我们就需要通过Android Lint的扩展机制自己定制lint规则。
自定义lint如何使用
自定义lint是一个纯java项目,以jar的形式输出。有了包含lint规则的jar后,有两种使用方案:
- 方案一:把此jar拷贝到 ~/.android/lint/ 目录中(文件名任意)。此时,这些lint规则针对所有项目生效。
- 方案二:继续创建一个Android library项目,用来输出包含lint.jar的aar;然后,让目标项目依赖此aar即可使自定义lint规则生效。
由于方案一是全局生效的策略,无法单独针对目标项目,用处不大。在工程实践中,我们主要使用方案二。
AAR是Android Library的一种新的二进制分发格式,它把资源也一起打包,这样一来图片和布局资源文件也能够被同时分发。AAR格式文件能够包含一个可选的lint.jar文件,如果一个app依赖了一个包含lint.jar的aar文件,那么这个lint.jar中的规则就会在app的lint任务中被用来做lint检查。
自定义lint实现原理
自定义lint规则是以jar形式存在的,主要通过继承两种类来实现扩展lint功能:
①继承IssueRegistry
:这是自定义Lint规则的主类或者叫注册类,有且仅有一个,用来注册这个自定义Lint项目中有哪些自定义的issue(issue就是需要lint检查出来并报告给用户的各种问题)需要被检测。
②继承Detector
并选择Detector中合适的XXXScanner
接口来实现:在这里根据自身业务需求,实现各种自定义探测器(Detector),并定义各种issue,根据自身需求的不同这样的类可以有一个或多个。
事实上,Android系统默认的lint检查功能是通过BuiltinIssueRegistry类来定义的,在这个类的源码中可以看到定义的各种issue、detector,如图:
com.android.tools.lint.detector.api.Detector提供了7种XXXScanner接口,根据自身需要选择合适的接口去实现,下面把这7个接口的信息列出:
1、JavaScanner
功能:Specialized interface for detectors that scan Java source file parse trees
2、ClassScanner
功能:Specialized interface for detectors that scan Java class files
3、BinaryResourceScanner
功能:Specialized interface for detectors that scan binary resource files
4、ResourceFolderScanner
功能:Specialized interface for detectors that scan resource folders (the folder directory itself, not the individual files within it)
5、XmlScanner
功能:Specialized interface for detectors that scan XML files
6、GradleScanner
功能:Specialized interface for detectors that scan Gradle files
7、OtherFileScanner
功能:Specialized interface for detectors that scan other files
实现自定义Lint规则的过程,实际上就是实现detector的过程,每个detector能够定义1个或多个不同类型的issue。也就是说,一个detector能够检测多种issue,这些issue在逻辑上是有关联的,但这些issue可以拥有不同的严重程度、描述等,并能够独立地被抑制(suppress,即禁用对该issue的检查)。
自定义lint实战
下面简单演示一下开发一个自定义Lint规则的完整流程。
【1】在Android Studio中,打开或新建一个工程,然后点击【File -> New -> New Module】,在弹出窗口中选择新建一个Java Library,如图:
我们这里把Java Library命名为ljflintrules。
【2】自定义lint规则需要继承一些特定的类,所以需要在ljflintrules的build.gradle中添加依赖:
1 | compile 'com.android.tools.lint:lint-api:24.3.1' |
【3】在ljflintrules中新建一个LoggerUsageDetector类,用来检测用户代码中是否使用了android.util.Log
类,如果有,就报告一个issue,代码如下:
1 | public class LoggerUsageDetector extends Detector |
这段代码中,我们定义了一个ISSUE,定义时传入的6个参数意义如下:
LogUtilsNotUseds
: 我们这条lint规则的id,这个id必须是独一无二的。You must use our 'LogUtils'
:对这条lint规则的简短描述。Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.
:对这条lint规则更详细的解释。Category.MESSAGES
:类别。9
:优先级,必须在1到10之间。Severity.ERROR
:严重程度。其他可用的严重程度还有FATAL、WARNING、INFORMATIONAL、IGNORE。Implementation
:这是连接Detector与Scope的桥梁,其中Detector的功能是寻找issue,而scope定义了在什么范围内查找issue。在我们的例子中,我们需要在字节码级别分析用户有没有使用android.util.Log
。
这个类中针对字节码中的android/util/Log进行了检查,并在发现时报告LogUtilsNotUsed这个issue。你也可以在这个类中定义多个issue,然后在代码逻辑中(比如checkCall方法中)针对不同的情况,抛出不同的issue。也就是说,一个XXXDetector是可以报告多种issue的。
如果需要检测更多问题,你也可以定义更多的XXXDetector类。XXXDetector类可以有多个。
【4】在ljflintrules中新建一个MyIssueRegistry类,它继承自IssueRegistry
。这个类用来注册我们自己定义了哪些issue,这样lint在检查代码时才知道要针对哪些issue进行检查。代码如下:
1 | public class MyIssueRegistry extends IssueRegistry { |
这个类中只有一个方法,就是返回一个List,其中包含了我们自定义的所有issue。
这里我们为了能够在控制台中清楚的看到我们自定义的lint规则是否被调用了,所以打印了一行提示信息。
【5】对于自定义lint生成的jar,我们必须在它的清单文件中指明它的主类。这里我们通过配置ljflintrules的build.gradle文件来完成这项工作:
1 | jar { |
现在,你可以在控制台中通过命令./gradlew ljflintrules:assemble
来执行编译任务,就可以输出我们需要的jar文件了。你可以在ljflintrules工程目录的build/libs/
下找到ljflintrules.jar。
如果你想验证这个jar文件是不是真的有效,可以把它拷贝到~/.android/lint/
目录下,然后在终端中输入lint --show LogUtilsNotUsed
看看有没有输出我们定义的issue信息,有则表明自定义lint成功,如图:
测试完后记得把它从~/.android/lint/
中删除。
【6】由于我们要把上一步生成的jar文件包含到一个aar中便于用户使用,所以我们还要在ljflintrules的build.gradle文件中添加以下信息:
1 | configurations { |
经过以上所有步骤,现在ljflintrules的build.gradle文件看起来是这样的:
1 | apply plugin: 'java' |
【7】新建一个Android Library项目,命名为ljflintrule_aar,用来输出aar,步骤如下:
在ljflintrule_aar的build.gradle的根节点加入以下内容:
1 | /* |
如果这时再编译项目,就会在ljflintrule_aar的输出目录中得到一个包含lint.jar的aar文件,这里的lint.jar就是我们在第5步中生成的ljflintrules.jar,只是换了个名字。
【8】在用户app中使用我们的自定义lint。
在用户自己的应用程序module中(我们这里就使用app module),打开app的build.gradle文件,在dependencies中加入以下依赖:
1 | compile project(':ljflintrule_aar') |
这里我们在app的MainActivity中使用了android自带的Log功能:
1 | public class MainActivity extends AppCompatActivity { |
在终端中,我们执行./gradlew lint
来执行lint任务,可以在终端中看到以下输出:
输出中指出发现了1个error和2个warning,并给出了详细报告的地址。
我们在浏览器中打开html格式的详细报告,如下图所示:
以上8个步骤完整演示了如何自定义lint并使用它。
小结
本文对于自定义Lint规则的介绍主要是集中在总体开发流程,给出了一个简单的实例。在实际开发过程中,我们比较常见的需求是针对xml布局文件、java源代码等内容进行某些检查,受Lint开发API的限制需要用到AST的相关知识,以及lombok.ast开源库。由于lombok.ast开源库几乎无文档可用,所以还是需要花一定时间来阅读这个库的源码,并熟悉SDK自带的Lint源码如何使用这个库。如果你对自定义Lint感兴趣,可以关注下一篇文章的相关介绍。